Descripción
Es claro que el confinamiento influyó en un mayor y acelerado crecimiento de la demanda de servicios de logística dado el boom del e-commerce. Y en este sentido, es esencial una muy buena planificación de los servicios de entrega, puesto que en estos días el único punto de contacto con el cliente en es cuando los productos llegan a casa.
FAST es una empresa de entrega de medicamentos y ha encomendado, por una parte, diseñar una modelo que prediga cuál es la probabilidad de que un despacho sea exitoso/fallido determinando los principales atributos explicativos, y por otro, entender cómo se segmentan los distintos tipos de despachos, con el fin de identificar donde se concentran los clusters con peor desempeño de servicio.
Datos
Se entregan datos del mes de agosto, el cual corresponde a un mes tipo con demanda normal (sin eventos especiales de aumentos de demanda). La data entregada corresponde a registros con las características de los pedidos ( dimesiones, categorías, tipos de clientes, etc.) y también sobre la trazabilidad del pedido durante el proceso y sus movimientos asociados.
Vector Objetivo
El vector objetivo corresponde a una variable binaria que determina si el pedido llego o no en la fecha comprometida, es 1 cuando la fecha de entrega real es menor o igual a la fecha de entrega comprometida, en otro caso no se cumple la entrega y es el resultado es 0.
Solución
La solución propuesta abarca en utilizar herramientas de google cloud para lograr dos objetivos:
Se trabajan con diferentes módulos, en caso de falta alguno se pueden instalar con:
#pandas
!pip install pandas
#numpy
!pip install numpy
#matplotlib
!pip install matplotlib
#seaborn
!pip install seaborn
#scipy
!pip install scipy
#networkx
!pip install networkx
#plotly
!pip install plotly
#yellowbrick
!pip install yellowbrick
Se importan librerias y funciones propias:
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import warnings
from scipy.stats import mode
from scipy import stats
from scipy.stats import ttest_ind
warnings.filterwarnings('ignore')
plt.rcParams['figure.figsize'] = (15,10)
plt.style.use('seaborn-darkgrid')
import plotly.offline as py
import plotly.graph_objects as go
import networkx as nx
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import LabelEncoder
from sklearn.ensemble import AdaBoostClassifier,GradientBoostingClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, accuracy_score, precision_score
from sklearn.metrics import classification_report,accuracy_score,roc_curve,auc,confusion_matrix,plot_confusion_matrix
from sklearn.metrics import roc_auc_score,precision_score,recall_score, make_scorer,f1_score
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_samples, silhouette_score
from yellowbrick.cluster import KElbowVisualizer, SilhouetteVisualizer
import pickle
from joblib import load, dump
from plotly.offline import iplot, init_notebook_mode
# Using plotly + cufflinks in offline mode
import cufflinks
cufflinks.go_offline(connected=True)
init_notebook_mode(connected=True)
import plotly.figure_factory as ff
pd.set_option('display.float_format', '{:.2f}'.format) ### para sacarle el formato exponencial al dataframe
import plotly.graph_objects as go
### funciones propias
from aux import *
Se entregan dos csv con información sobre los pedidos:
datos_med.csv: presenta detalle de diferentes atributos de cada pedido
datos_fact.csv: presenta todos los movimientos que se hicieron en la vida del pedido.
Ambos csv se extraen desde la base de datos del cliente, para trabajarlos se subieron al ambiente de Google Cloud-Storage y por medio de BigQuery se genero una tabla que contenga los datos entregados con sus atributos.
%%bigquery df
select * from `charged-ground-301216.test_1_fast_project.datos_med`
Se revisa tamaño del dataframe y cantidad de atirbutos:
df.shape
Son datos de 2.568.900 registros, correspondiente a los pedidos del mes de Agosto-20.
Se revisa presencia de datos nulos o perdidos con función auxiliar:
missing_values_table(df)
No se encontraron registros con datos perdidos o nulos, se puede continuar con el procesamiento.
Se genera un nuevo atributo en la base que se calcula a partir las dimensiones de peso, volumen, alto y largo. Se llama peso equivalente, donde se evalua entre el peso físico versus el peso volumétrico, quedando el mayor valor como el seleccionado. El peso volumétrico se refiere una convencion de que en un metro cúbico equivalen a 250 kgs ( largo x ancho x alto / 4000).
En base a esto se considera además, la propia clasificación tarifaria de FAST que diferencia al peso equivalente en 4 categorías:
Detalle de la transformación:
%%bigquery df
select CASE
WHEN ( ( (largo * ancho * alto) / 4000 ) > peso ) THEN CASE
WHEN ( ( (largo * ancho * alto) / 4000 ) <= 1.5 ) THEN 'peq'
WHEN ( ( (largo * ancho * alto) / 4000 ) > 1.5 and ( (largo * ancho * alto) / 4000 ) <= 3 ) THEN 'med'
WHEN ( ( (largo * ancho * alto) / 4000 ) > 3 and ( (largo * ancho * alto) / 4000 ) <= 4.5 ) THEN 'gran'
ELSE 'sob'
END
ELSE CASE
WHEN ( peso <= 1.5 ) THEN 'peq'
WHEN ( peso > 1.5 and peso <= 3 ) THEN 'med'
WHEN ( peso > 3 and peso <= 4 ) THEN 'gran'
ELSE 'sob'
END
END as dimension_pedido
,lower(replace(tipo_cliente,' ','_')) as tipo_cliente
,lower(replace(tipo_medicamento,' ','_')) as tipo_medicamento
,comuna_origen
,comuna_destino
,region_origen
,region_destino
,((tiene_nombre_destinatario * 2) + (tiene_correo_destinatario * 2) + (tiene_telefono_destinatario * 6)) as score_datos_contactabilidad_destintario
,((tiene_nombre_remitente * 2) + (tiene_correo_remitente * 2) + (tiene_telefono_remitente * 6)) as score_datos_contactabilidad_remitente
,lower(replace(velocidad_servicio,' ','_')) as velocidad_servicio
,lower(replace(tipo_envio,' ','_')) as tipo_envio
,lower(replace(tipo_induccion,' ','_')) as tipo_induccion
,case when sla_compromiso=1 then 'si' else 'no' end cumplimiento
,valor_contratado
,distancia_envio_mts
,satisfaccion_cliente
,dia_semana_admision
,dia_semana_entrega
,lower(replace(tipo_servicio,' ','_')) as tipo_servicio
,case when (admite_a_tiempo_para_p__ck_up = 1) THEN 'si' else 'no' end as admite_a_tiempo_para_pick_up
,horas_desde_creacion_hasta_compromiso
,case when horas_desde_creacion_hasta_compromiso<=48 then 'menor_48' else 'mayor_48' end tramo_hr_desp
,horas_desde_creacion_hasta_salida_primera_milla
from `charged-ground-301216.test_1_fast_project.datos_med`
Descripción de cada variable en el dataframe:
dimension_pedido: (str) variable categórica que clasifica si el pedido es pequeño (peq), mediano (med), grande (gran) y sobredimensionado (sob).tipo_cliente:(str) variable categórica que define si el cliente es una persona natural (persona) o una empresa (empresa).tipo_medicamento:(str) variable categórica que define si el medicamento enviado es de alto valor o normal.comuna_origen: (str) variable que representa el código de la comuna desde donde se envia el medicamento.comuna_destino:(str) variable que representa el código de la comuna hacia donde se envia el medicamento.region_origen:(str) variable que representa el código de la región desde donde se envia el medicamento.region_destino:(str) variable que representa el código de la región hacia donde se envia el medicamento.score_datos_contactabilidad_destintario: (int) valor de escala de contactabilidad del destinatario, es un puntaje entregado por datos de datos personales, mail y teléfono de contactabilidad.score_datos_contactabilidad_remitente:(int) valor de escala de contactabilidad del remitente, es un puntaje entregado por datos de datos personales, mail y teléfono de contactabilidad.velocidad_servicio: (str) variable categórica que define la velocidad del servicio contratado va desde mismo dia hasta plus_1.tipo_envio:(str) variable categórica que define si el pedido fue enviado a la puerta o por ventanilla.tipo_induccion:(str) variable categórica que define si el drop es e sucursal o retirado al cliente.cumplimiento: (str) vector objetivo que explica si el pedido llego o no en la promesa de entrega realizada al inicio del proceso.valor_contratado: (float) valor asociado a la entrega del pedido.distancia_envio_mts:(float) valor asociado a la distancia para entregar el pedido.tipo_servicio:(str) variable categorica que define si el pedido es una entrega local o interregional.admite_a_tiempo_para_pick_up: (str) variable categórica que clasifica si el pedido fue admitido o no a tiempo para pickup.horas_desde_creacion_hasta_compromiso: (float) valor que refleja el número de horas totales que se tiene para entregar el pedido hasta el compromiso.tramo_hr_desp: (str) variable que define si el despacho se entregó en un tiempo menor o igual 48 horas, o bien superior a este.dia_semana_admision: (str) variable categórica que toma marcas lunes a domingo y define el día en que se crea el despacho en el sistema de FAST.dia_semana_entrega: (str) variable categórica que toma marcas lunes a domingo y define el día en que se entrega el despacho.horas_desde_creacion_hasta_salida_primera_milla:(float) valor que refleja el número de horas transcurridas desde la creación hasta la salida en primera milla.Sobre las principales variables descriptivas:
df.describe()
Sobre los conjuntos de la data a revisar, se trabajara con el 100% del conjunto de datos entregado para la modelación y para el entranmiento de modelos se dividirá en dos grupos de Test/Training con un % de 70 y 30.
Para la validación del modelo se contrastará los resultados de los modelos con un nuevo dataset correspondiente a otro mes o periodo.
El vector objetivo corresponde a una variable binaria que determina si el pedido llego o no en la fecha comprometida, es 1 cuando la fecha de entrega real es menor o igual a la fecha de entrega comprometida, en otro caso no se cumple la entrega y es el resultado es 0. Sobre su comportamiento:
eda_plots(pd.DataFrame(df['cumplimiento']),1)
Revisamos la distribucion de los atributos:
atributos=list(df.columns)
atributos.remove('cumplimiento')
eda_plots(df.loc[:,atributos],4)
A continuación se revisa más a fondo el desbalanceo de estos atributos, al revisarlos con el vector objetivo.
Se revisa cada atributo categórico y cómo es el cumplimiento del compromiso para cada segmento,
Cumplimiento según tamaño
g = sns.catplot(x="cumplimiento", col="dimension_pedido", col_wrap=4,data=df,kind="count", height=2.5, aspect=1.8)
Cumplimiento según tipo cliente
g = sns.catplot(x="cumplimiento", col="tipo_cliente", col_wrap=4,data=df,kind="count", height=2.5, aspect=1.8)
Cumplimiento según tipo medicamento
g = sns.catplot(x="cumplimiento", col="tipo_medicamento", col_wrap=4,data=df,kind="count", height=2.5, aspect=1.8)
Cumplimiento según velocidad servicio
g = sns.catplot(x="cumplimiento", col="velocidad_servicio", col_wrap=4,data=df,kind="count", height=2.5, aspect=1.8)
Cumplimiento según tipo envio
g = sns.catplot(x="cumplimiento", col="tipo_envio", col_wrap=4,data=df,kind="count", height=2.5, aspect=1.8)
Cumplimiento según tipo de inducción
g = sns.catplot(x="cumplimiento", col="tipo_induccion", col_wrap=4,data=df,kind="count", height=2.5, aspect=1.8)
Cumplimiento según tipo de servicio
g = sns.catplot(x="cumplimiento", col="tipo_servicio", col_wrap=4,data=df,kind="count", height=2.5, aspect=1.8)
Cumplimiento según tiempo de pickup
g = sns.catplot(x="cumplimiento", col="admite_a_tiempo_para_pick_up", col_wrap=4,data=df,kind="count", height=2.5, aspect=1.8)
Cumplimiento según día de creación del despacho
g = sns.catplot(x="cumplimiento", col="dia_semana_admision", col_wrap=4,data=df,kind="count", height=2.5, aspect=1.8)
Cumplimiento según día de entrega del despacho
g = sns.catplot(x="cumplimiento", col="dia_semana_entrega", col_wrap=4,data=df,kind="count", height=2.5, aspect=1.8)
Cumplimiento según tiempo de entrega
g = sns.catplot(x="cumplimiento", col="tramo_hr_desp", col_wrap=4,data=df,kind="count", height=2.5, aspect=1.8)
Sobre las variables continuas, se revisa una matriz de correlaciones:
corr_matrix = df[['valor_contratado','distancia_envio_mts','horas_desde_creacion_hasta_compromiso','horas_desde_creacion_hasta_salida_primera_milla','satisfaccion_cliente']]
corrs = corr_matrix.corr()
figure = ff.create_annotated_heatmap(
z=corrs.values,
x=list(corrs.columns),
y=list(corrs.index),
annotation_text=corrs.round(4).values,
showscale=True)
figure
Sobre las variables categorias, se arman dummies y luego se revisa matriz:
df_3 = df.loc[:,['dimension_pedido','tipo_cliente','tipo_medicamento','velocidad_servicio','tipo_envio','tipo_induccion','tipo_servicio','admite_a_tiempo_para_pick_up']]
df_dum = pd.get_dummies(df_3, columns=['dimension_pedido','tipo_cliente','tipo_medicamento','velocidad_servicio','tipo_envio','tipo_induccion','tipo_servicio','admite_a_tiempo_para_pick_up'], drop_first=True)
plt.figure(figsize=(40,30))
corrs = df_dum.corr()
figure = ff.create_annotated_heatmap(
z=corrs.values,
x=list(corrs.columns),
y=list(corrs.index),
annotation_text=corrs.round(4).values,
showscale=True)
for i in range(len(figure.layout.annotations)):
figure.layout.annotations[i].font.size = 8
figure.show()
Se realizan diagramas de cajas para revisar presencia de outliers:
box_plots(corr_matrix,2)
Se puede ver una gran cantidad de valores fuera de las cajas, lo que muestra alta presencia de valores escapados, sin embargo, pueden estar muy relacionados con la escala de los valores, por ejemplo en el caso del valor contratado el mínimo es 1.73 y el máximo sobre pasa los 6 millones,todo esto nos indica que una normalización del tipo logarítmica nos ayudaría a regularizar los datos. Se probará esta transformación cuando se realicen los modelos de clusterización y clasificación.
Para efectos de conocer el comportamiento de los atributos continuos según el vector objetivo, se procede a aplicar un Test de medias y así vislumbrar potenciales atributos con significancia estadística puedan ser usados en los posteriores análisis y modelos de este proyecto.
mean_test(df,'valor_contratado')
mean_test(df,'distancia_envio_mts')
mean_test(df,'horas_desde_creacion_hasta_compromiso')
mean_test(df,'horas_desde_creacion_hasta_salida_primera_milla')
En este apartado el objetivo es encontrar los principales tipos de despachos que realiza FAST, y así determinar cuáles son los más propensos a que sean fallidos al término de su trayecto. Esto ayudaría a proponer acciones correctivas y enfocadas en el negocio.
En esta línea, desde una mirada exploratoria se definirán las variables son más ilustrativas para segmentar según una visualización de redes, aplicando teoría de grafos.
Finalmente, se implementará un modelo de clustering K-Means y el método de Elbow para definir cuántos grupos se pueden usar para la segmentación.
Se genera una nueva categoria segun los cuartiles de la distancia en metros:
df['tipo_tramo'] = np.where(df['distancia_envio_mts'].between(0, 16025),'TRAMO CORTO',
np.where(df['distancia_envio_mts'].between(16026,90530),'TRAMO MEDIO',
np.where(df['distancia_envio_mts'].between(90531, 464025),'TRAMO LARGO',
'TRAMO EXTRA' ) ))
Revisamos la distribucion de la nueva cagtegoria y vemos que queda de manera bastante balanceada:
sns.countplot(x="tipo_tramo", data=df);
atr = dict(df['tipo_cliente'].value_counts())
atr.update(dict(df['tipo_envio'].value_counts()))
atr.update(dict(df['tipo_induccion'].value_counts()))
atr.update(dict(df['tipo_servicio'].value_counts()))
atr.update(dict(df['tipo_tramo'].value_counts()))
atr.update(dict(df['tramo_hr_desp'].value_counts()))
related = {}
for ix, it in df.iterrows():
tc = it['tipo_cliente']
te = it['tipo_envio']
ti = it['tipo_induccion']
ts = it['tipo_servicio']
tt = it['tipo_tramo']
th = it['tramo_hr_desp']
#print(tc,te, ti,ts, tt)
if tc not in related:
related[tc] = {}
if te not in related[tc]:
d = {te: 1}
related[tc].update(d)
related[te] = {}
else:
d = {te: related[tc][te] + 1}
related[tc].update(d)
if ti not in related[te]:
d = {ti:1}
related[te].update(d)
related[ti] = {}
else:
d = {ti: related[te][ti] + 1}
related[te].update(d)
if ts not in related[ti]:
d = {ts:1}
related[ti].update(d)
related[ts] = {}
else:
d = {ts: related[ti][ts] + 1}
related[ti].update(d)
if tt not in related[ts]:
d = {tt:1}
related[ts].update(d)
related[tt] = {}
else:
d = {tt: related[ts][tt] + 1}
related[ts].update(d)
if th not in related[tt]:
d = {th:1}
related[tt].update(d)
related[th] = {}
else:
d = {th: related[tt][th] + 1}
related[tt].update(d)
g = nx.Graph()
# Add node for each character
for at in atr.keys():
if atr[at] > 0:
g.add_node(at, size = atr[at])
for rel in related.keys():
for co_rel in related[rel].keys():
# Only add edge if the count is positive
if related[rel][co_rel] > 0:
g.add_edge(rel, co_rel, weight = related[rel][co_rel])
pos_ = nx.spring_layout(g,iterations=10)
# For each edge, make an edge_trace, append to list
edge_trace = []
for edge in g.edges():
if g.edges()[edge]['weight'] > 0:
char_1 = edge[0]
char_2 = edge[1]
x0, y0 = pos_[char_1]
x1, y1 = pos_[char_2]
text = char_1 + '--' + char_2 + ': ' + str(g.edges()[edge]['weight'])
trace = make_edge([x0, x1, None], [y0, y1, None], text,
0.000004*g.edges()[edge]['weight'])
edge_trace.append(trace)
node_trace = go.Scatter(x = [],
y = [],
text = [],
textposition = "top center",
textfont_size = 10,
mode = 'markers+text',
hoverinfo = 'none',
marker = dict(color = [],
size = [],
line = None))
# For each node in midsummer, get the position and size and add to the node_trace
for node in g.nodes():
x, y = pos_[node]
node_trace['x'] += tuple([x])
node_trace['y'] += tuple([y])
node_trace['marker']['color'] += tuple(['cornflowerblue'])
node_trace['marker']['size'] += tuple([0.000007*g.nodes()[node]['size']])
node_trace['text'] += tuple(['<b>' + node + '</b>'])
layout = go.Layout(
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)'
)
#grafico
fig = go.Figure(layout = layout)
for trace in edge_trace:
fig.add_trace(trace)
fig.add_trace(node_trace)
fig.update_layout(showlegend = False)
fig.update_xaxes(showticklabels = False)
fig.update_yaxes(showticklabels = False)
fig.show()
#label encoder
le = LabelEncoder()
#se crea dataframe con atributos del cluster
clustering = df.loc[:,['tipo_cliente','tipo_envio','tipo_induccion'
,'tipo_servicio','tipo_tramo','distancia_envio_mts'
,'horas_desde_creacion_hasta_compromiso']].copy()
categorical_cols = ['tipo_cliente','tipo_envio','tipo_induccion'
,'tipo_servicio','tipo_tramo']
float_cols = ['distancia_envio_mts','horas_desde_creacion_hasta_compromiso']
# para las variables categoricas se transforman con label enconder y para las continuas una normalizacion logaritmica
clustering[categorical_cols] = clustering[categorical_cols].apply(lambda col: le.fit_transform(col))
clustering[float_cols] = clustering[float_cols].apply(lambda col: np.log1p(col))
clustering.head(10)
model = KMeans()
visualizer = KElbowVisualizer(model, k=(2,10),timings=True)
visualizer.fit(clustering)
visualizer.poof();
El gráfico muestra que con 4 clusters se entrega el modelo más eficiente, segun la metrica de 'distortion' que se calcula comparando la suma de las distancias cuadradas para cada centro.
visualizer = KElbowVisualizer(model, k=(2,10), metric='calinski_harabasz', timings=True)
visualizer.fit(clustering) # Fit the data to the visualizer
visualizer.poof();
Utilizando la métrica de 'calinski harabasz' que se basa en el radio de dispersión de entre y para cada cluster, el número más eficiente para el modelo también son 4 grupos.
Dado los resultados mostrados anteriormente, se procede a aplicar el modelo KMeans con un número de 4 grupos, obteniendo los siguientes resultados:
model = KMeans(4,random_state=465)
model.fit(clustering)
Se agrega el atributo del cluster al que pertenece cada registro a nuestro dataframe para revisar su distribución,
df['cluster'] = model.labels_
df['cluster'].value_counts('%')
Se puede ver que la mayoría de los registros pertenecen al cluster 0 con un 43% de la data, luego al cluster 3 con un 31%, al cluster 1 con el 22% y finalmente al cluster 2 con el 3%.
Se renombra cada cluster con las siguientes categorias:
df["desc_cluster"] = df["cluster"].replace([0,2,3,1]
,["Business Package","Los Patiperros","Los Enfocados","Los Poco Optimizados"])
Se grafican en boxplots las atirbutos continuos para cada cluster y ver su comportamiento:
cob_columns_cont = []
cob_columns_cate = []
for index, (columnas_df,serie) in enumerate(df.iteritems()):
if pd.api.types.is_float_dtype(serie) is True:
cob_columns_cont.append(columnas_df)
else:
if pd.api.types.is_integer_dtype(serie) is True:
cob_columns_cont.append(columnas_df)
else:
cob_columns_cate.append(columnas_df)
cob_columns_cont.remove("cluster")
cob_columns_cate.remove("desc_cluster")
cob_columns_cate.remove("comuna_origen")
cob_columns_cate.remove("comuna_destino")
plt.figure(figsize=(25,15))
for index,col in enumerate(cob_columns_cont):
plt.subplot(3,3,index +1)
sns.boxplot(x=df['desc_cluster'], y = df[col])
Conclusiones,
plt.figure(figsize=(25,25))
for index,col in enumerate(cob_columns_cate):
plt.subplot(6,3,index +1)
sns.countplot(x=col, data = df, hue="desc_cluster")
Conclusiones,
Como se revisó en la seccion de analisis descriptivo, las variables continuas presentaban una alta variabilidad en escala y presencia de outliers, por lo que se decide por aplicar una trasnformacion logaritmica antes de continuar con el desarrollo del modelo predictivo.
### Logaritmo y graficos
continuas =['valor_contratado','distancia_envio_mts','horas_desde_creacion_hasta_compromiso','horas_desde_creacion_hasta_salida_primera_milla']
df['valor_contratado'] = np.log1p(df['valor_contratado'])
df['distancia_envio_mts'] = np.log1p(df['distancia_envio_mts'])
df['horas_desde_creacion_hasta_compromiso'] = np.log1p(df['horas_desde_creacion_hasta_compromiso'])
df['horas_desde_creacion_hasta_salida_primera_milla'] = np.log1p(df['horas_desde_creacion_hasta_salida_primera_milla'])
eda_plots(df[continuas],2)
Ahora se nota una distribución más parecida a una normal, por lo que se opta por trabajar los datos de esta manera. Se observan valores cero para el atributo distancia_envio_mts (3.2%), esto se da porque las distancias se calculan entre centros operacionales, y en algunos casos de tipos de envios "LOCALES", las distribución se realiza por el mismo centro operacional que la admite.
df["distancia_envio_mts"].describe()
df["distancia_envio_mts"].replace([0],df["distancia_envio_mts"].describe()[4],inplace = True)
Se revisan resultados del cambio y se aprecia un comportamiento mucho más normalizado que antes,
eda_plots(df[continuas],2)
df=df.drop(columns=['comuna_destino','comuna_origen','satisfaccion_cliente','dia_semana_entrega','tramo_hr_desp','dia_semana_admision','tipo_tramo','cluster','desc_cluster'])
df.columns
Se procede a binarizar las variables que son del tipo categórico:
df_modelo = pd.get_dummies(df, columns=['dimension_pedido','region_origen',
'region_destino','tipo_cliente',
'tipo_medicamento','velocidad_servicio',
'tipo_envio','tipo_induccion',
'tipo_servicio','admite_a_tiempo_para_pick_up'], drop_first=True)
df_modelo["cumplimiento"].replace(["si","no"]
,[1,0],inplace = True)
columnas = df_modelo.columns
cols = list(columnas)
cols.remove("cumplimiento")
X_train, X_test,y_train,y_test = train_test_split(df_modelo.loc[:,cols],df_modelo["cumplimiento"],test_size=.33,random_state=202101)
%%time
#Modelo Generico AdaBoostClassifier
Modelo_AdaBoost = AdaBoostClassifier()
Modelo_AdaBoost.fit(X_train, y_train)
y_hat_AdaBoost = Modelo_AdaBoost.predict(X_test)
metrics_model(Modelo_AdaBoost,X_test,y_test)
#dump(Modelo_AdaBoost, "aboost_model.joblib")
A nivel general, el modelo de AdaBoostClassifier tiene buen nivel de prediccion, con un accuracy del 75,4%. Sin embargo, al revisar su desempeño por categoria vemos que presenta un bajo rendimiento para los pedidos 0, es decir, los que no cumplen la entrega segun su promesa, con un recall de 54% lo cual es bastante bajo.
%%time
dtc_model = DecisionTreeClassifier()
dtc_model.fit(X_train,y_train)
y_hat_dtc = dtc_model.predict(X_test)
metrics_model(dtc_model,X_test,y_test)
#dump(dtc_model, "dtc_model.joblib")
A nivel general, el modelo de DecisionTreeClassifier tiene buen nivel de predicción, con un accuracy del 77,2%. Al revisar su desempeño por categoria vemos que presenta buen rendimiento para los dos tipos de pedidos, tanto los que cumplen como los que no cumplen. Se tiene presente que este tipo de modelo (arbol) tiende a un overfit sobre la data de entrenamiento, por lo que luego se realizará validación con una muestra de otro periodo.
%%time
log_model = LogisticRegression()
log_model.fit(X_train,y_train)
y_hat_log = log_model.predict(X_test)
metrics_model(log_model,X_test,y_test)
#dump(log_model, "log_model.joblib")
A nivel general, el modelo de Regresion Logistica tiene buen nivel de predicción, con un accuracy del 72,5%. Sin embargo, al revisar su desempeño por categoría vemos que presenta un bajo rendimiento para los pedidos 0, es decir, los que no cumplen la entrega según su promesa, con un recall de 49% lo cual es bastante bajo al no superar el 50%.
%%time
gboost_model = GradientBoostingClassifier()
gboost_model.fit(X_train, y_train)
y_hat_GBoost = gboost_model.predict(X_test)
metrics_model(gboost_model,X_test,y_test)
#dump(gboost_model, "gboost_model.joblib")
A nivel general, el modelo de GradientBoostClassifier tiene buen nivel de prediccion, con un accuracy del 77,3%. Sin embargo, al revisar su desempeño por categoría vemos que presenta un bajo rendimiento para los pedidos 0, es decir, los que no cumplen la entrega segun su promesa, con un recall de 51% lo cual es bastante bajo.
A nivel general el accuracy de todos los modelos se mueve entre el 72% y el 77%, lo cual es un nivel aceptable de desempeño del modelo, siendo el mejor el Gradient Boosting:
El objetivo del modelo es predecir el cumplimiento de la promesa de entrega que se le notifica al cliente, en este sentido nos es más relevante poder identificar de manera correcta los pedidos que no cumplirian con el tiempo comprometido, es decir los de clasificacion 0, por este motivo al revisar el indicador de desempeño del recall para los clase 0:
Se puede ver que el modelo de DecisionTree presenta el mejor desempeño por bastante rango versus el resto de los modelos, con un 72% de recall para las categorias 0. Tambien se validaron los modelo con una muestra de datos de otro periodo, el accuraccy y metricas de desmepeño bajaron en aprox 5 puntos porcentuales, pero la conclusion es la misma, el modelo de Decision Tree entrega los resultados mas acertados. (Para revisar validacion revisar notebook adjunto: 'Validación_Modelos_FAST.ipynb')
Si revisamos los atributos más importantes para el modelo de Decision Tree podemos ver que:
best_attr = plot_importance(dtc_model,X_train.columns)
El atributo más importante para el modelo son la distancia en metros, luego las horas de creación hasta la salida de la primera milla y las horas de creación hasta la fecha comprometida, es decir, cuanto tiempo de plazo se tiene para la entrega.
Se traen las categorías obtenidas en con el modelo de clusterizacion y se seleccionan los grupos, mayores a 10.000 pedidos que tengan una baja probabilidad de cumplimiento según el modelo de predicción generado.
%%bigquery clusters
select *
from `charged-ground-301216.test_1_fast_project.cluster_persona`
columnas = df_modelo.columns
cols = list(columnas)
cols.remove("cumplimiento")
yhat_dtc=dtc_model.predict(df_modelo.loc[:,cols])
clusters['pred']=yhat_dtc
grupos=clusters.groupby(['cluster', 'tipo_cliente','tipo_envio', 'tipo_induccion', 'tipo_tramo']).pred.agg(['count', 'mean'])
grupos[(grupos['count']>10000) & (grupos['mean']<0.6)].sort_values(by='count', ascending=False).sort_values(by=['count','mean'], ascending=False)
Se ve que existen 15 grupos principales al evaluar los peores grupos con un Q de pedidos que son representativos para la muestra, y principalmente se ve que: